The Classic FPS Pack developed by Ajay Venkat and Thomas Brush, was created as a way to make developing Simple FPS Games easier. Most FPS Assets are bloated and hard to understand, this pack contains scripts that are easy to modify and prefabs that are easy to understand to help you quickly setup a prototype.
It is currently being used in an upcoming indie game Father by Thomas Brush.
Recommended Approach:
Before following any documentation ensure that the setup is completed. To setup, use the Classic FPS > Setup editor and follow all the steps to have the necessary layers, tags, physics settings etc.
Ensure you go through one by one and do these, each one is important for the pack to function (except 4):
Setup Tags : Setup all the tags needed for the Pack
Setup Layers : Setup all the layers needed to run the Pack
Setup Physics Collisions : Setup all the layer vs layer collisions (ex. Enemy doesn't collide with Enemy)
Setup Fixed Timestep : Make the game physics run more smooth
Add Scenes : Add all the demo scenes to Build Settings
Setup Active Input Handling : This ensures that both the new and old input system are used so that all systems can be used with this pack. [Restart Required!]
This pack has been documented in 2 forms, both in video form as well as written form. The video documentation contains information about each of the systems within the pack and will give you a good starting point to use them.
This site will give a more detailed account of every script in the pack and give you the groundwork to extend it.
Link to Playlist: Video Documentation Playlist
This section covers the following:
There are demo prefabs and scenes show casing all of the features and scripts talked about above, please check them out and use them as a starting point for creating your own prefabs.
The Dialogue is a Scriptable Object that holds all of your Dialogue data. You can create a new Dialogue by right clicking on your Project folder and Create > Classic FPS > Dialogue.
In this Dialogue object you can fill out all the Dialogue you need and serialize it and deserialize it into a text file.
Here is a good tutorial on how to create new Dialogue: Creating New Dialogue
The Dialogue is just a holder of Dialogue Interactions, when you use a Dialogue Scriptable Object, in the Inspector a custom editor will be used to allow you to create/delete/edit the Dialogue Interactions.
The Dialogue object once you have a reference to it can be queried about different information.
interactionName: Name of the interaction between Player and NPC to store as Text File.
PlayerName: The name of the player, default "Player"
NPCName: The name of the NPC, default "NPC"
interactions: A List of DialogueInteraction which make up this Dialogue
ValidIndex (int index): Is there an interaction at this Dialogue Index
CurrentDialogueChild (bool a, int interactionIndex): Get the a or b child of the interactionIndex.
bool CurrentDialogueHasOptions (bool a, bool b, int interactionIndex): Get whether or not a, b, or both exist for interactionIndex index.
List
Most of the time you will not need to interface directly with the Dialogue object, there are other classes that will handle this information.
The Dialogue Interaction contains the actual dialogue data such as the text spoken, who is speaking and the index of the dialogue.
Here is a good tutorial on how to create new Dialogue: Creating New Dialogue
isPlayer: Does this dialogue belong to the player?
waitTime: How long to wait before moving to the next dialogue interaction
Line: The dialogue line to show on screen
Commands: List of strings which is the metadata of the current dialogue, this is mostly used for things such as gifting keys during a conversation. Take a look at Key Dialogue.
When entering this within text file you can do it like this:
- This is a dialogue line [COMMAND1, COMMAND2]
Or in the editor you can edit it through comma seperated values.
sfx: The sound effect to play, this can be the voice acted line for this Dialogue. In the Editor you will be able to match the waitTime with the dialogue.
The Dialogue object holds a 1 dimensional list of Dialogue Interactions, so to traverse this list with a parent child relationship each object should keep track of itself and it's children interactions.
index: Current index of this interaction
childAIndex: Index of the first child of this interaction
childBIndex: Index of the second child of this interaction
parentIndex: Index of th parent of this interaction
Limitations of this system include that each interaction has a maximum of 2 possible pathways, this is not dynamic at the moment. This might take a bit of work to extend; however the way that the serialization & deserialization works this shouldn't be too difficult.
The Dialogue Processor is found on the Canvas and controls all the UI elements of the Dialogue and also handles moving through a Dialogue and sending back callbacks.
This is directly executed by the DialogueRunner and most of the time you will not need to work with this script. However you can and should change the UI that is linked to this processor to change the visuals.
These options are for the input & SFX of selection.
OptionSelected: The Sound Effect to play when an option is selected.
OptionAInput: The Input Action (key) to press to select the first option.
OptionBInput: The Input Action (key) to press to select the second option.
These functions can be used to directly interfere with the UI of the Dialogue:
Disable(): Disables the current dialogue by force.
BeginDialogue(Dialogue dialogue, string NPCName, string PlayerName, float waitTime, bool playPlayerReplies, System.Action
You can begin the dialogue manually by passing in all of these variables that are usually found on the DialogueRunner, please look at DialogueRunner to understand how to call this function.
The Dialogue Runner is a MonoBehaviour that can be attached to Game Objects and can execute Dialogue; it is the interface between the Player Input and the running of dialogue on UI.
It also has a list of public functions that you can use to control how Dialogue is executed and actions to run during and after the execution of Dialogue.
In order for this to work you will need any sort of Trigger attached to the same object.
Here is a good tutorial on how to use the Dialogue System: Dialogue System
The References to the UI itself is now held in the UIManager script, which should also be on the Canvas.
triggerEnabled: Is the trigger for this object enabled
source: AudioSource from which to play the SFX on the Dialogue
autoRunWhenPlayerEnters: Auto-run the Dialogue when the Player enters the trigger
diableAfterLeavingTrigger: Auto-disable the Dialogue when the Player leaves the trigger
runOnce: Ensure that the Dialogue only runs once through
playPlayerReplies: Should you play the Player's options once they have selected it or just move to the next NPC dialogue
DialogueCompletedEvent: This is an UnityEvent meaning you can attach any function on other scripts to this callback, that function will be called when the Dialogue is completed.
DialogueRunEvent: This is also an UnityEvent meaning you can attach any function on other scripts to this callback, that function will be called everytime a new DialogueInteraction is shown on screen and it will also pass the DialogueInteraction to the parameters of that function.
This can be used to check the commands at every interaction to execute some functionality.
runnableDialogues: The list of Dialogues that can be executed, you can attach the scriptable objects you created in the project to this list.
defaultRunDialogue: This is the index of the dialogue in runnableDialogues to execute by default when the player enters the trigger or uses the input.
dialogueRunInput: The input to use to execute this Dialogue, this is another way to begin the Dialogue; the player still has to be within the trigger.
overrideDialogueInputExecution: Prevents input from working so that another script can take over the execution of the Dialogue Runner.
ExecuteDialogue (int index, bool requirePlayerToBeInTrigger): From another script you can execute a Dialogue at a certain runnableDialogues index, you can also ensure the player is in the trigger when this is executed.
ExecuteDialogue (Dialogue interaction, bool requirePlayerToBeInTrigger): Here you can pass a Dialogue directly into the runner and ensure the player is within the trigger.
StopCurrentDialogue(): This will stop the currently executing dialogue.
The Dialogue Utils are in charge of serializing and deserializing the Dialogue. At the moment Dialogue is serialized into a text file which usually looks something like this:
- Hello! Are you looking for an orange key?
- No...
- Okay, Bye!
- Yes!
- Okay, take this ORANGE key!! [KEY]
- Thanks random NPC!
You can use the public static functions of the Dialogue Utils if you want to serialize and deserailize yourself.
You can access these functions like so:
DialogueUtils.StaticFunction(parameter);
string Serialize (List
List
The Door script is attached to any object that can be triggered with a Key object, you can even use it for treasure chests and padlocks.
The Door is derived from State so its tracked by the SaveManager.
saveDoorState: Whether or not to save the state of the door.
keyReference: A drop down where you can select or type the ID of the key you want to use to unlock this door. You can select anything from the KeyManager.
requiresKey: Do you need a key to open this door?
doorOpen: SFX to play On Door Open.
dialogueRunner: A reference to the DialogueRunner component on the GameObject that will play dialogue.
executeDialogue: Reference to the actual Dialogue object to run when the door doesn't open.
openDoor: The Input Action to use when opening the door (you can change this in inspector).
The Door will contain an animator with the boolean parameter opened; when this is set to true the Door's open animation will play.
When the player enters the trigger and then presses the openDoor input action, the door will check if the Player has the key needed and open the door.
If not then it will play the 'no key' dialogue. You can extend this to play dialogue when the door opens.
The only tracked state is opened, so if the Player opens the door they will not have to keep reopening it after they die; it can remain open.
The Key Dialogue is a script that be attached to an NPC alongside a DialogueRunner to allow an NPC to gift the player a key depending on the choices they make.
Please watch this video understand how to attach actions to a dialogue : Attaching Dialogue Interactions
The Key Dialogue also derives from state so it is tracked by the SaveManager.
keyReference: The Key ID that should be given to the player when the dialogue interaction reaches the key gifting.
dialogue: Dialogue Runner reference.
onlyGiftOnce: Prevent the NPC from gifting the key more than once.
When the player runs the dialogue, at every step of the dialogue the GiftKey (DialogueInteraction interaction) function in this script is called. This is done by attaching it as a callback on the DialogueRunner.
When the function is called it also returns the current interaction the dialogue is at, the interaction contains all the information about that dialogue point including all the commands attached to it.
The GiftKey function checks the following:
if interaction.Commands.Contains("KEY"), if this is true then it will gift the player the key. This is a command and they can be attached to any section of the dialogue in the following way.
- Hello have this key [KEY]
If the player has never recieved a key before the Key Dialogue will play the dialogue at the 1 index of the dialogues on the Dialogue Processor. Otherwise it will play the 2 index dialogue.
Please take a look at the prefabs in the Dialogue folder to understand how to set up this system fully.
The tracked state ensures that the NPC only provides the player with the key once, so they can't restart the game and ask for another key.
This is a sample dialogue tree structure that can gift the player a key:
This is a class derived from the Pickup class so it tracks the state of whether or not this item has been picked up.
Note: This item needs a trigger.
This will pickup a new key and add it to the collected keys, then these keys can be used to open doors.
The keys are appended through the PlayerStatistics script using the CollectKey(string keyID) function.
The Key Reference is just a class with a string inside it, it is serializable and the only purpose is to allow a custom editor to be generated.
The Custom Editor will check if there is a Key Manager in the project and develop a drop down that you can select the key from, this makes adding Key Selection variables easier in the code.
However, if you have multiple Key Managers it will revert to a text field.
The Classic Enemy is used on two enemy types:
The Shooter Enemy: Chases the Player and shoots projectiles at them, when it gets closes enough performs attacks.
The Walker Enemy: Simply chases the Player until close enough then attacks.
This class derives from the Enemy script so the properties and methods from that script can be used here.
injuryRadius: The radius at which the Enemy can attack the player at a close distance, this is an attack form such a punching.
injuryDelay: The delay between each attack the Enemy makes.
damageByProximity: The amount of damage the Player will take when the Enemy is attacking at close range.
aimSpeed: The speed at which the Enemy can aim their weapon.
shootProjectiles: Does this Enemy shoot any projectiles.
projectilePrefab: The projectile prefab to shoot.
gunModel: This gun model reference will point towards the player with the speed of aimSpeed.
projectileSpawnPoint: Spawn point of the projectile which is a child of the gunModel so its facing in the right direction.
projectileSpeed: Initial speed of the projectile.
The Follow() method is called when the Player is in the trigger on Update and the agent's location is set to the Player; all animations are triggered and shooting is triggered from this method as well.
The Update loop is in the Enemy script from which this script is inherited from.
The Damageable Entity is a base class that is extended to other objects in this pack such as the Breakable Object and Enemy.
It is based on the State class so it will be tracked by the SaveManager.
The Damageable Entity contains a few functions that can be overriden by children classes that mainly cover taking damage and dying.
health: Initial health of the entity.
respawnOnLoad: Ensure that the death of this entity is not permenant.
hitParticles: Emit hit particles when the entity takes damage.
droppablePrefabs: List of prefabs that can be dropped when the entity dies.
chanceOfDrop: The chance that this entity drops something when it dies.
alwaysDrop: Always drop some prefabs on the death of the entity.
dropAllItems: Drop all items in droppablePrefabs on death.
spawnOffset: Offset of the droppable prefab spawn on death.
onTakeDamage: Sound effect to play on take damage.
onDeath: Sound effect to play when the entity dies.
The override functions are used by any scripts that inherit this script.
TakeDamage (float damage, float delay): What functionality to execute when the entity takes damage, the default behaviour is to emit hit particles, take away the health, check for death and play the sound effects.
Die (bool spawnDrops): What to execute when the entity dies, the default behaviour is to spawn the drops and then set the GameObject to disabled.
The only property tracked in this state is the health of the entity, if the health is <= 0 after it is loaded in then it will be killed automatically; however no drops will spawn.
Basically, the system will ensure the entity remains dead or destroyed until saves are cleared.
The Drone Enemy script is only used on one demo enemy, the Drone Enemy; the Drone Enemy is able to fly above in the air and shoot homing missiles at the Player.
This class derives from the Enemy script so the properties and methods from that script can be used here.
aimSpeed: Speed at which drone can lock onto the Player.
followSpeed: Speed at which the drone can follow the Player.
stoppingRadius: Radius at which the drone stops from the Player.
injuryDelay: Delay of each projectile shot.
hitCollider: This is the collider of the drone, the center of the collider needs to be relaigned with the geometry each frame to ensure the collisions remain working.
geometry: The visual geometry of the Drone.
projectilePrefab: Prefab that will be spawned as the Projectile.
projectileSpawnPoint: The spawn point of the Projectile.
The Follow() method is called when the Player is in the trigger on Update and the agent's location is set to the Player; all animations are triggered and shooting is triggered from this method as well.
The Update loop is in the Enemy script from which this script is inherited from.
The Enemy is a base class that is used for the two types of default enemies in this pack; the script itself is derived from DamageableEntity meaning it can take damage and die.
trigger: The trigger that the player enters in order to grasp the attention of the Enemy.
damageByThroughObjectsMultiplier: The damage provided to the Enemy is based on the velocity of an object thrown at it; this value multiplies that by a certain amount.
agent: The NavMesh agent that the is going to traverse over.
animator: The Enemy animator that will play its Walking & Attacking animations.
graphics: The Graphics object on the Enemy (ex. Capsule).
These are the methods that can be used by classes that inherit from Enemy:
Follow (): The behaviour when the Enemy is in the follow state; when the Player has entered the trigger.
Attack (bool longRange): The behaviour of the attack when the player is close to the Enemy and far away.
It is important that the Enemy is standing on a NavMesh and the NavMesh is baked.
A Breakable Object is derived from DamageableEntity meaning its health is tracked and it is able to take damage and eventually die.
Breakable Objects are items such as Crates that can be attacked by weapons or be destroyed by throwing them.
Damageable Entities including this one are able to drop objects on their death, and play death and damage SFX.
Most of the public variables are drawn from the DamageableEntity script; however there are a few variables:
damageVelocityMultiplier: The amount to multiply the velocity of the object on impact to take damage with.
minimumVelocityForImpact: Minimum amount of velocity needed to take damage.
By default the Player can move Rigidbodies; however sometimes this can lead to unstable or unwanted behaviour. The Pushable Object script allows you to control some of those parameters.
This script is also responsible for allowing an Object in the scene to be picked up and thrown either through a Gravity Gun or by the default player pickup.
It also handles things such as playing an impact sound effect.
The PlayerPhysics script interacts with the Pushable Objects the most; especially in with the pushPower and pushableObjectVelocityMax variables; please read PlayerPhysics first to understand the effect on Pushable Objects.
A few things affect the movement of the Pushable objects:
pushPower of PlayerPhysics: How much force to apply to the Object that you are pushing up against.
pushableObjectVelocityMax of PlayerPhysics: If this is not limited then the Player can add infinite force to an Object.
speed of PlayerController: How much speed the player has when pushing against this object.
The properties of the Rigidbody attached to the Pushable object
The public variables mostly affect the behaviour of the object:
canBePickedUp: This determines whether or not the player can pickup the object either by default or through a Gravity Gun.
maximumVelocity: Safety option to ensure object never goes above a certain velocity (angular velocity is also impacted here).
minimumVelocityBeforeAudio: The minimum velocity on impact to create an impact sound.
objectHoldingOffset: This is a manual value that determines how far away to hold the object when it is picked up.
precalculateBounds: If this is true then the previous parameter is rendered useless; the precalculate bounds attempts to estimate where to place the object depending on its size.
impactSound: Sound when the object collides with something.
onShootSound: Sound to play when the object is shot.
waitBeforePlayingSFXAgain: Minimum delay between two SFX.
In the Rigidbody component you will only have access to one drag parameter; so if you want rigid movement on X/Z axis without affecting how the object falls; this is not possible.
This is why further drag options are provided here to affect individual axis:
For example, you want a simple cube that you can push on the ground. Simply create a Cube GameObject, then follow these steps:
Assign the PushableObject Component to the object
Ensure there is a Collider on the object
Ensure there is a Rigidbody on the object
If you want the Cube to just move along the surface of the ground then you can Freeze Rotation on the _X_ and _Z_ axis.
If you don't want the Cube to rotate too much then you can bump up the Angular Drag.
The GameObject would behave something like this:
Lets bump up the X & Z drag on the Pushable Object script, this will lead to some nicer behaviour: